Um guia completo sobre Server Actions no Next.js 14, abordando as melhores práticas de manipulação de formulários, validação de dados, considerações de segurança e técnicas avançadas para construir aplicações web modernas.
Server Actions no Next.js 14: Dominando as Melhores Práticas de Manipulação de Formulários
O Next.js 14 introduz funcionalidades poderosas para construir aplicações web performáticas e fáceis de usar. Entre elas, as Server Actions destacam-se como uma forma transformadora de lidar com submissões de formulários e mutações de dados diretamente no servidor. Este guia oferece uma visão abrangente das Server Actions no Next.js 14, focando nas melhores práticas para manipulação de formulários, validação de dados, segurança e técnicas avançadas. Exploraremos exemplos práticos e forneceremos insights acionáveis para ajudá-lo a construir aplicações web robustas e escaláveis.
O que são as Server Actions do Next.js?
As Server Actions são funções assíncronas que são executadas no servidor e podem ser invocadas diretamente de componentes React. Elas eliminam a necessidade de rotas de API tradicionais para lidar com submissões de formulários e mutações de dados, resultando em um código simplificado, segurança aprimorada e melhor desempenho. As Server Actions são Componentes de Servidor React (RSCs), o que significa que são executadas no servidor, levando a carregamentos de página iniciais mais rápidos e melhor SEO.
Principais Benefícios das Server Actions:
- Código Simplificado: Reduza o código repetitivo eliminando a necessidade de rotas de API separadas.
- Segurança Aprimorada: A execução no lado do servidor minimiza as vulnerabilidades do lado do cliente.
- Desempenho Melhorado: Execute mutações de dados diretamente no servidor para tempos de resposta mais rápidos.
- SEO Otimizado: Aproveite a renderização no lado do servidor para uma melhor indexação pelos motores de busca.
- Segurança de Tipos (Type Safety): Beneficie-se da segurança de tipos de ponta a ponta com o TypeScript.
Configurando seu Projeto Next.js 14
Antes de mergulhar nas Server Actions, certifique-se de que você tem um projeto Next.js 14 configurado. Se estiver começando do zero, crie um novo projeto usando o seguinte comando:
npx create-next-app@latest my-next-app
Certifique-se de que seu projeto está usando a estrutura de diretório app
para aproveitar ao máximo os Server Components e as Actions.
Manipulação Básica de Formulários com Server Actions
Vamos começar com um exemplo simples: um formulário que envia dados para criar um novo item em um banco de dados. Usaremos um formulário simples com um campo de entrada e um botão de envio.
Exemplo: Criando um Novo Item
Primeiro, defina uma função de Server Action dentro do seu componente React. Esta função cuidará da lógica de submissão do formulário no servidor.
// app/components/CreateItemForm.tsx
'use client';
import { useState } from 'react';
async function createItem(formData: FormData) {
'use server'
const name = formData.get('name') as string;
// Simula a interação com o banco de dados
console.log('Criando item:', name);
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simula a latência
console.log('Item criado com sucesso!');
}
export default function CreateItemForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
await createItem(formData);
setIsSubmitting(false);
}
return (
);
}
Explicação:
- A diretiva
'use client'
indica que este é um componente de cliente. - A função
createItem
é marcada com a diretiva'use server'
, indicando que é uma Server Action. - A função
handleSubmit
é uma função do lado do cliente que chama a server action. Ela também gerencia o estado da UI, como desabilitar o botão durante a submissão. - A propriedade
action
do elemento<form>
é definida como a funçãohandleSubmit
. - O método
formData.get('name')
recupera o valor do campo de entrada 'name'. - O
await new Promise
simula uma operação de banco de dados e adiciona latência.
Validação de Dados
A validação de dados é crucial para garantir a integridade dos dados e prevenir vulnerabilidades de segurança. As Server Actions oferecem uma excelente oportunidade para realizar a validação no lado do servidor. Essa abordagem ajuda a mitigar os riscos associados apenas à validação do lado do cliente.
Exemplo: Validando Dados de Entrada
Modifique a Server Action createItem
para incluir a lógica de validação.
// app/components/CreateItemForm.tsx
'use client';
import { useState } from 'react';
async function createItem(formData: FormData) {
'use server'
const name = formData.get('name') as string;
if (!name || name.length < 3) {
throw new Error('O nome do item deve ter pelo menos 3 caracteres.');
}
// Simula a interação com o banco de dados
console.log('Criando item:', name);
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simula a latência
console.log('Item criado com sucesso!');
}
export default function CreateItemForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setErrorMessage(null);
try {
await createItem(formData);
} catch (error: any) {
setErrorMessage(error.message || 'Ocorreu um erro.');
} finally {
setIsSubmitting(false);
}
}
return (
{errorMessage && {errorMessage}
}
);
}
Explicação:
- A função
createItem
agora verifica se oname
é válido (pelo menos 3 caracteres). - Se a validação falhar, um erro é lançado.
- A função
handleSubmit
é atualizada para capturar quaisquer erros lançados pela Server Action e exibir uma mensagem de erro ao usuário.
Usando Bibliotecas de Validação
Para cenários de validação mais complexos, considere usar bibliotecas de validação como:
- Zod: Uma biblioteca de declaração e validação de esquemas com foco em TypeScript.
- Yup: Um construtor de esquemas JavaScript para analisar, validar e transformar valores.
Aqui está um exemplo usando Zod:
// app/utils/validation.ts
import { z } from 'zod';
export const CreateItemSchema = z.object({
name: z.string().min(3, 'O nome do item deve ter pelo menos 3 caracteres.'),
});
// app/components/CreateItemForm.tsx
'use client';
import { useState } from 'react';
import { CreateItemSchema } from '../utils/validation';
async function createItem(formData: FormData) {
'use server'
const name = formData.get('name') as string;
const validatedFields = CreateItemSchema.safeParse({ name });
if (!validatedFields.success) {
return { errors: validatedFields.error.flatten().fieldErrors };
}
// Simula a interação com o banco de dados
console.log('Criando item:', name);
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simula a latência
console.log('Item criado com sucesso!');
}
export default function CreateItemForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setErrorMessage(null);
try {
await createItem(formData);
} catch (error: any) {
setErrorMessage(error.message || 'Ocorreu um erro.');
} finally {
setIsSubmitting(false);
}
}
return (
{errorMessage && {errorMessage}
}
);
}
Explicação:
- O
CreateItemSchema
define as regras de validação para o camponame
usando Zod. - O método
safeParse
tenta validar os dados de entrada. Se a validação falhar, ele retorna um objeto com os erros. - O objeto
errors
contém informações detalhadas sobre as falhas de validação.
Considerações de Segurança
As Server Actions aumentam a segurança ao executar o código no servidor, mas ainda é crucial seguir as melhores práticas de segurança para proteger sua aplicação contra ameaças comuns.
Prevenindo Cross-Site Request Forgery (CSRF)
Ataques CSRF exploram a confiança que um site tem no navegador de um usuário. Para prevenir ataques CSRF, implemente mecanismos de proteção CSRF.
O Next.js lida automaticamente com a proteção CSRF ao usar Server Actions. O framework gera e valida um token CSRF para cada submissão de formulário, garantindo que a solicitação se origine da sua aplicação.
Lidando com Autenticação e Autorização de Usuários
Garanta que apenas usuários autorizados possam realizar certas ações. Implemente mecanismos de autenticação e autorização para proteger dados e funcionalidades sensíveis.
Aqui está um exemplo usando NextAuth.js para proteger uma Server Action:
// app/components/CreateItemForm.tsx
'use client';
import { useState } from 'react';
import { getServerSession } from 'next-auth';
import { authOptions } from '../../app/api/auth/[...nextauth]/route';
async function createItem(formData: FormData) {
'use server'
const session = await getServerSession(authOptions);
if (!session) {
throw new Error('Não autorizado');
}
const name = formData.get('name') as string;
// Simula a interação com o banco de dados
console.log('Criando item:', name, 'pelo usuário:', session.user?.email);
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simula a latência
console.log('Item criado com sucesso!');
}
export default function CreateItemForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setErrorMessage(null);
try {
await createItem(formData);
} catch (error: any) {
setErrorMessage(error.message || 'Ocorreu um erro.');
} finally {
setIsSubmitting(false);
}
}
return (
{errorMessage && {errorMessage}
}
);
}
Explicação:
- A função
getServerSession
recupera as informações da sessão do usuário. - Se o usuário não estiver autenticado (sem sessão), um erro é lançado, impedindo a execução da Server Action.
Higienizando Dados de Entrada
Higienize os dados de entrada para prevenir ataques de Cross-Site Scripting (XSS). Ataques XSS ocorrem quando código malicioso é injetado em um site, potencialmente comprometendo os dados do usuário ou a funcionalidade da aplicação.
Use bibliotecas como DOMPurify
ou sanitize-html
para higienizar a entrada fornecida pelo usuário antes de processá-la em suas Server Actions.
Técnicas Avançadas
Agora que cobrimos o básico, vamos explorar algumas técnicas avançadas para usar as Server Actions de forma eficaz.
Atualizações Otimistas
As atualizações otimistas proporcionam uma melhor experiência do usuário ao atualizar imediatamente a UI como se a ação fosse bem-sucedida, mesmo antes de o servidor confirmar. Se a ação falhar no servidor, a UI é revertida para seu estado anterior.
// app/components/UpdateItemForm.tsx
'use client';
import { useState } from 'react';
async function updateItem(id: string, formData: FormData) {
'use server'
const name = formData.get('name') as string;
// Simula a interação com o banco de dados
console.log('Atualizando item:', id, 'com o nome:', name);
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simula a latência
// Simula uma falha (para fins de demonstração)
const shouldFail = Math.random() < 0.5;
if (shouldFail) {
throw new Error('Falha ao atualizar o item.');
}
console.log('Item atualizado com sucesso!');
return { name }; // Retorna o nome atualizado
}
export default function UpdateItemForm({ id, initialName }: { id: string; initialName: string }) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [itemName, setItemName] = useState(initialName);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setErrorMessage(null);
// Atualiza a UI de forma otimista
const newName = formData.get('name') as string;
setItemName(newName);
try {
const result = await updateItem(id, formData);
// Se for bem-sucedido, a atualização já está refletida na UI através do setItemName
} catch (error: any) {
setErrorMessage(error.message || 'Ocorreu um erro.');
// Reverte a UI em caso de erro
setItemName(initialName);
} finally {
setIsSubmitting(false);
}
}
return (
Nome Atual: {itemName}
{errorMessage && {errorMessage}
}
);
}
Explicação:
- Antes de chamar a Server Action, a UI é imediatamente atualizada com o novo nome do item usando
setItemName
. - Se a Server Action falhar, a UI é revertida para o nome original do item.
Revalidando Dados
Depois que uma Server Action modifica os dados, pode ser necessário revalidar os dados em cache para garantir que a UI reflita as últimas alterações. O Next.js oferece várias maneiras de revalidar dados:
- Revalidate Path: Revalida o cache para um caminho específico.
- Revalidate Tag: Revalida o cache para dados associados a uma tag específica.
Aqui está um exemplo de revalidação de um caminho após a criação de um novo item:
// app/components/CreateItemForm.tsx
'use client';
import { useState } from 'react';
import { revalidatePath } from 'next/cache';
async function createItem(formData: FormData) {
'use server'
const name = formData.get('name') as string;
// Simula a interação com o banco de dados
console.log('Criando item:', name);
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simula a latência
console.log('Item criado com sucesso!');
revalidatePath('/items'); // Revalida o caminho /items
}
export default function CreateItemForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setErrorMessage(null);
try {
await createItem(formData);
} catch (error: any) {
setErrorMessage(error.message || 'Ocorreu um erro.');
} finally {
setIsSubmitting(false);
}
}
return (
{errorMessage && {errorMessage}
}
);
}
Explicação:
- A função
revalidatePath('/items')
invalida o cache para o caminho/items
, garantindo que a próxima solicitação para esse caminho busque os dados mais recentes.
Melhores Práticas para Server Actions
Para maximizar os benefícios das Server Actions, considere as seguintes melhores práticas:
- Mantenha as Server Actions Pequenas e Focadas: As Server Actions devem executar uma única tarefa bem definida. Evite lógicas complexas dentro das Server Actions para manter a legibilidade e a testabilidade.
- Use Nomes Descritivos: Dê às suas Server Actions nomes descritivos que indiquem claramente seu propósito.
- Lide com Erros de Forma Elegante: Implemente um tratamento de erros robusto para fornecer feedback informativo ao usuário e evitar que a aplicação quebre.
- Valide os Dados Completamente: Realize uma validação de dados abrangente para garantir a integridade dos dados e prevenir vulnerabilidades de segurança.
- Proteja suas Server Actions: Implemente mecanismos de autenticação e autorização para proteger dados e funcionalidades sensíveis.
- Otimize o Desempenho: Monitore o desempenho de suas Server Actions e otimize-as conforme necessário para garantir tempos de resposta rápidos.
- Utilize o Cache de Forma Eficaz: Aproveite os mecanismos de cache do Next.js para melhorar o desempenho e reduzir a carga no banco de dados.
Armadilhas Comuns e Como Evitá-las
Embora as Server Actions ofereçam inúmeras vantagens, existem algumas armadilhas comuns a serem observadas:
- Server Actions Excessivamente Complexas: Evite colocar muita lógica dentro de uma única Server Action. Divida tarefas complexas em funções menores e mais gerenciáveis.
- Negligenciar o Tratamento de Erros: Sempre inclua tratamento de erros para capturar erros inesperados e fornecer feedback útil ao usuário.
- Ignorar as Melhores Práticas de Segurança: Siga as melhores práticas de segurança para proteger sua aplicação de ameaças comuns como XSS e CSRF.
- Esquecer de Revalidar os Dados: Garanta que você revalide os dados em cache após uma Server Action modificar os dados para manter a UI atualizada.
Conclusão
As Server Actions do Next.js 14 fornecem uma maneira poderosa e eficiente de lidar com submissões de formulários e mutações de dados diretamente no servidor. Seguindo as melhores práticas descritas neste guia, você pode construir aplicações web robustas, seguras e performáticas. Adote as Server Actions para simplificar seu código, aumentar a segurança e melhorar a experiência geral do usuário. Ao integrar esses princípios, considere o impacto global de suas escolhas de desenvolvimento. Garanta que seus formulários e processos de manipulação de dados sejam acessíveis, seguros e fáceis de usar para diversos públicos internacionais. Esse compromisso com a inclusividade não apenas melhorará a usabilidade de sua aplicação, mas também ampliará seu alcance e eficácia em escala global.